package com.starsky.common.mongodb.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.mongodb.client.ListIndexesIterable;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.starsky.common.mongodb.entity.MongoDBGroupByParams;
import com.starsky.common.mongodb.utils.PageResult;
import com.starsky.common.page.PageData;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @desc: MongoDB 操作工具类
 * @author: wangsh
 * @time: 2020/9/28 10:24
 */
@Component
public class BaseMongoDBHelper<T> {

    private static final int FIRST_PAGE_NUM = 1;
    private static final String ID = "_id";

    @Autowired
    private MongoTemplate mongoTemplate;


    /**
     * 功能描述: 创建一个集合
     * 同一个集合中可以存入多个不同类型的对象，我们为了方便维护和提升性能，
     * 后续将限制一个集合中存入的对象类型，即一个集合只能存放一个类型的数据
     *
     * @param name 集合名称，相当于传统数据库的表名
     * @return:void
     */
    public void createCollection(String name) {
        mongoTemplate.createCollection(name);
    }

    /**
     * 功能描述: 创建索引
     * 索引是顺序排列，且唯一的索引
     *
     * @param collectionName 集合名称，相当于关系型数据库中的表名
     * @param filedName      对象中的某个属性名
     * @return:java.lang.String
     */
    public String createIndex(String collectionName, String filedName) {
        //配置索引选项
        IndexOptions options = new IndexOptions();
        // 设置为唯一
        options.unique(true);
        //创建按filedName升序排的索引
        return mongoTemplate.getCollection(collectionName).createIndex(Indexes.ascending(filedName), options);
    }


    /**
     * 功能描述: 获取当前集合对应的所有索引的名称
     *
     * @param collectionName
     * @return:java.util.List<java.lang.String>
     */
    public List<String> getAllIndexes(String collectionName) {
        ListIndexesIterable<Document> list = mongoTemplate.getCollection(collectionName).listIndexes();
        //上面的list不能直接获取size，因此初始化arrayList就不设置初始化大小了
        List<String> indexes = new ArrayList<>();
        for (Document document : list) {
            document.entrySet().forEach((key) -> {
                //提取出索引的名称
                if (key.getKey().equals("name")) {
                    indexes.add(key.getValue().toString());
                }
            });
        }
        return indexes;
    }

    /**
     * 功能描述: 往对应的集合中插入一条数据
     *
     * @param info           存储对象
     * @param collectionName 集合名称
     * @return:void
     */
    public void insert(T info, String collectionName) {
        mongoTemplate.insert(info, collectionName);
    }

    /**
     * 功能描述: 往对应的集合中批量插入数据，注意批量的数据中不要包含重复的id
     *
     * @param infos 对象列表
     * @return:void
     */

    public void insertMulti(List<T> infos, String collectionName) {
        mongoTemplate.insert(infos, collectionName);
    }

    /**
     * 功能描述: 使用索引信息精确更改某条数据
     *
     * @param id             唯一键
     * @param collectionName 集合名称
     * @param info           待更新的内容
     * @return:void
     */
    public void updateById(String id, String collectionName, T info) {
        Query query = new Query(Criteria.where("id").is(id));
        Update update = new Update();
        JSONObject jQuery = JSON.parseObject(JSON.toJSONString(info));
        jQuery.forEach((key, value) -> {
            //因为id相当于传统数据库中的主键，这里使用时就不支持更新，所以需要剔除掉
            if (!key.equals("id")) {
                update.set(key, value);
            }
        });
        mongoTemplate.updateMulti(query, update, info.getClass(), collectionName);
    }

    /**
     * 功能描述: 根据id删除集合中的内容
     *
     * @param id             序列id
     * @param collectionName 集合名称
     * @param clazz          集合中对象的类型
     * @return:void
     */

    public void deleteById(String id, Class<T> clazz, String collectionName) {
        // 设置查询条件，当id=#{id}
        Query query = new Query(Criteria.where("id").is(id));
        // mongodb在删除对象的时候会判断对象类型，如果你不传入对象类型，只传入了集合名称，它是找不到的
        // 上面我们为了方便管理和提升后续处理的性能，将一个集合限制了一个对象类型，所以需要自行管理一下对象类型
        // 在接口传入时需要同时传入对象类型
        mongoTemplate.remove(query, clazz, collectionName);
    }

    /**
     * 功能描述: 根据id查询信息
     *
     * @param id             注解
     * @param clazz          返回实体类对象类型
     * @param collectionName 集合名称
     * @return:java.util.List<T>
     */
    public T selectById(String id, Class<T> clazz, String collectionName) {
        // 查询对象的时候，不仅需要传入id这个唯一键，还需要传入对象的类型，以及集合的名称
        return mongoTemplate.findById(id, clazz, collectionName);
    }

    /**
     * 功能描述: 查询列表信息
     * 将集合中符合对象类型的数据全部查询出来
     *
     * @param collectName 集合名称
     * @param clazz       返回实体类类型
     * @return:java.util.List<T>
     */
    public List<T> selectList(String collectName, Class<T> clazz) {
        PageData<T> pageData = selectList(collectName, clazz, null, null);
        return pageData.getList();
    }

    /**
     * 功能描述: 分页查询列表信息
     *
     * @param collectName 集合名称
     * @param clazz       返回实体类对象类型
     * @param currentPage 当前页码
     * @param pageSize    分页大小
     * @return:java.util.List<T>
     */
    public PageData<T> selectList(String collectName, Class<T> clazz, Integer currentPage, Integer pageSize) {
        //设置分页参数
        Query query = new Query();
        //设置分页信息
        if (!ObjectUtils.isEmpty(currentPage) && ObjectUtils.isEmpty(pageSize)) {
            query.skip((currentPage - 1) * pageSize).limit(pageSize);
        }
        // 查询记录总数
        int totalCount = (int) mongoTemplate.count(query, clazz);
        //总记录
        List<T> list = mongoTemplate.find(query, clazz, collectName);

        return new PageData<>(list, totalCount);
    }


    /**
     * 功能描述: 根据条件查询集合
     *
     * @param collectName 集合名称
     * @param conditions  查询条件，目前查询条件处理的比较简单，仅仅做了相等匹配，没有做模糊查询等复杂匹配
     * @param clazz       返回实体类对象类型
     * @param currentPage 当前页码
     * @param pageSize    分页大小
     * @return:java.util.List<T>
     */

    public PageData<T> selectByPage(String collectName, Map<String, String> conditions, Class<T> clazz, Integer currentPage, Integer pageSize) {
        if (ObjectUtils.isEmpty(conditions)) {
            return selectList(collectName, clazz, currentPage, pageSize);
        } else {
            //设置分页参数
            Query query = new Query();
            query.skip(currentPage).limit(pageSize);
            // 往query中注入查询条件
            conditions.forEach((key, value) -> query.addCriteria(Criteria.where(key).is(value)));
            // 查询记录总数
            int totalCount = (int) mongoTemplate.count(query, clazz);
//            总记录
            List<T> list = mongoTemplate.find(query, clazz, collectName);

            return new PageData(list, totalCount);
        }
    }

    /**
     * 功能描述: 根据条件分页查询集合
     *
     * @param collectName 集合名称
     * @param query       查询条件
     * @param clazz       返回实体类对象类型
     * @return
     */
    public PageData<T> selectByPage(String collectName, Query query, Class<T> clazz) {
        // 查询记录总数
        int totalCount = (int) mongoTemplate.count(query, clazz);
//            总记录
        List<T> list = mongoTemplate.find(query, clazz, collectName);

        return new PageData(list, totalCount);
    }

    /**
     * 功能描述: 根据条件查询集合
     *
     * @param collectName 集合名称
     * @param query       查询条件
     * @param clazz       对象类型
     * @return
     */
    public List<T> selectByQuery(String collectName, Query query, Class<T> clazz) {
        List<T> result = mongoTemplate.find(query, clazz, collectName);
        return result;
    }

    /**
     * 分页查询，直接返回集合类型的结果.
     */
    public <T> PageResult<T> pageQuery(Query query, Class<T> entityClass, Integer pageSize,
                                       Integer pageNum) {
        return pageQuery(query, entityClass, pageSize, pageNum, Function.identity(), null);
    }

    /**
     * 分页查询，直接返回集合类型的结果.
     */
    public <T> PageResult<T> pageQuery(Query query, Class<T> entityClass, Integer pageSize,
                                       Integer pageNum, String sortField, Sort.Direction order) {
        return pageQueryBySortField(query, entityClass, pageSize, pageNum, Function.identity(), sortField, order);
    }

    /**
     * 分页查询，不考虑条件分页，直接使用skip-limit来分页.
     */
    public <T, R> PageResult<R> pageQuery(Query query, Class<T> entityClass,
                                          Integer pageSize, Integer pageNum, Function<T, R> mapper) {
        return pageQuery(query, entityClass, pageSize, pageNum, mapper, null);
    }

    /**
     * 分页查询.
     *
     * @param query       Mongo Query对象，构造你自己的查询条件.
     * @param entityClass Mongo collection定义的entity class，用来确定查询哪个集合.
     * @param mapper      映射器，你从db查出来的list的元素类型是entityClass, 如果你想要转换成另一个对象，比如去掉敏感字段等，可以使用mapper来决定如何转换.
     * @param pageSize    分页的大小.
     * @param pageNum     当前页.
     * @param lastId      条件分页参数, 区别于skip-limit，采用find(_id>lastId).limit分页.
     *                    如果不跳页，像朋友圈，微博这样下拉刷新的分页需求，需要传递上一页的最后一条记录的ObjectId。 如果是null，则返回pageNum那一页.
     * @param <T>         collection定义的class类型.
     * @param <R>         最终返回时，展现给页面时的一条记录的类型。
     * @return PageResult，一个封装page信息的对象.
     */
    public <T, R> PageResult<R> pageQuery(Query query, Class<T> entityClass,
                                          Integer pageSize, Integer pageNum, Function<T, R> mapper, String lastId) {
        //分页逻辑
        long total = mongoTemplate.count(query, entityClass);
        final Integer pages = (int) Math.ceil(total / (double) pageSize);
        if (pageNum <= 0 || pageNum > pages) {
            pageNum = FIRST_PAGE_NUM;
        }
        final Criteria criteria = new Criteria();
        if (StringUtils.isNotBlank(lastId)) {
            if (pageNum != FIRST_PAGE_NUM) {
                criteria.and(ID).gt(new ObjectId(lastId));
            }
            query.limit(pageSize);
        } else {
            int skip = pageSize * (pageNum - 1);
            query.skip(skip).limit(pageSize);
        }

        final List<T> entityList = mongoTemplate
                .find(query.addCriteria(criteria)
                                .with(Sort.by(Sort.Direction.ASC, ID)),
                        entityClass);

        final PageResult<R> pageResult = new PageResult<>();
        pageResult.setTotal(total);
        pageResult.setPages(pages);
        pageResult.setPageSize(pageSize);
        pageResult.setPageNum(pageNum);
        if (mapper != null) {
            pageResult.setList(entityList.stream().map(mapper).collect(Collectors.toList()));
        } else {
            pageResult.setList((List<R>) entityList);
        }
        return pageResult;
    }


    /**
     * 分页查询.
     *
     * @param query       Mongo Query对象，构造你自己的查询条件.
     * @param entityClass Mongo collection定义的entity class，用来确定查询哪个集合.
     * @param mapper      映射器，你从db查出来的list的元素类型是entityClass, 如果你想要转换成另一个对象，比如去掉敏感字段等，可以使用mapper来决定如何转换.
     * @param pageSize    分页的大小.
     * @param pageNum     当前页.
     *                    如果不跳页，像朋友圈，微博这样下拉刷新的分页需求，需要传递上一页的最后一条记录的ObjectId。 如果是null，则返回pageNum那一页.
     * @param <T>         collection定义的class类型.
     * @param <R>         最终返回时，展现给页面时的一条记录的类型。
     * @return PageResult，一个封装page信息的对象.
     */
    public <T, R> PageResult<R> pageQueryBySortField(Query query, Class<T> entityClass,
                                                     Integer pageSize, Integer pageNum, Function<T, R> mapper, String sortField, Sort.Direction order) {
        //分页逻辑
        long total = mongoTemplate.count(query, entityClass);
        final Integer pages = (int) Math.ceil(total / (double) pageSize);
        if (pageNum <= 0 || pageNum > pages) {
            pageNum = FIRST_PAGE_NUM;
        }
        int skip = pageSize * (pageNum - 1);
        query.skip(skip).limit(pageSize);
        if (StringUtils.isNotEmpty(sortField) && order != null) {
            query.with(Sort.by(order, sortField));
        }
        final List<T> entityList = mongoTemplate
                .find(query.with(Sort.by(order, sortField)),
                        entityClass);
        final PageResult<R> pageResult = new PageResult<>();
        pageResult.setTotal(total);
        pageResult.setPages(pages);
        pageResult.setPageSize(pageSize);
        pageResult.setPageNum(pageNum);
        if (mapper != null) {
            pageResult.setList(entityList.stream().map(mapper).collect(Collectors.toList()));
        } else {
            pageResult.setList((List<R>) entityList);
        }
        return pageResult;
    }

    /**
     * 分组查询
     *
     * @param params
     * @param Clazz
     * @param <T>
     * @return
     */
    public <T> List<T> pageGroupQueryBySortField(MongoDBGroupByParams params, Class<T> Clazz) {
        Aggregation aggregation;
        GroupOperation add = Aggregation.group(params.getGroupName());
        for (String param : params.getSearch()) {
            add = add.first(param).as(param);
        }
        SortOperation sort = Aggregation.sort(params.getSortDes(), params.getSort());
        SkipOperation skip = Aggregation.skip(params.getPageNo());
        LimitOperation limit = Aggregation.limit(params.getPagteSize());
        aggregation = Aggregation.newAggregation(Aggregation.match(params.getCriteria()), add, sort, skip, limit);
        AggregationResults results = mongoTemplate.aggregate(aggregation, params.getCollectionName(), Clazz);
        List<T> list = results.getMappedResults();
        return list;
    }


}
